﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using King.Engine.GameObjects;

namespace King.Engine.Core
{
    public static class Screen
    {

        #region Fields

        private static SpriteBatch _spriteBatch;
        private static GraphicsDeviceManager _graphics;

        private static Matrix _cameraTransformation;

        private static Point _cameraLocation;
        private static Vector2 _cameraLocationVector;
        private static int _cameraWidth;
        private static int _cameraHeight;
        private static float _zoom;

        private static Rectangle _zoomedCameraBounds;
        private static Vector2 _zoomedCameraLocationVector;

        private static int _windowWidth;
        private static int _windowHeight;

        #endregion
        
        #region Properties

        /// <summary>
        /// Function that adjusts the draw depth for all future draw calls.
        /// By default, this is the identity function
        /// </summary>
        public static Func<float, float> AdjustDrawDepth;

        /// <summary>
        /// Get or set the size of the world
        /// </summary>
        public static Point WorldSize;

        /// <summary>
        /// Get or set the width of the game window
        /// </summary>
        public static int WindowWidth
        {
            get
            {
                return _windowWidth;
            }
            set
            {
                _windowWidth = value;
                _graphics.PreferredBackBufferWidth = value;
                _graphics.ApplyChanges();

                UpdateTransformation();
            }
        }

        /// <summary>
        /// Get or set the height of the game window
        /// </summary>
        public static int WindowHeight
        {
            get
            {
                return _windowHeight;
            }
            set
            {
                _windowHeight = value;
                _graphics.PreferredBackBufferHeight = value;
                _graphics.ApplyChanges();

                UpdateTransformation();
            }
        }

        /// <summary>
        /// Get or set the width of the camera
        /// </summary>
        public static int CameraWidth
        {
            get
            {
                return _cameraWidth;
            }
            set
            {
                _cameraWidth = value;

                UpdateTransformation();
                UpdateCameraBounds();
            }
        }

        /// <summary>
        /// Get or set the height of the camera
        /// </summary>
        public static int CameraHeight
        {
            get
            {
                return _cameraHeight;
            }
            set
            {
                _cameraHeight = value;

                UpdateTransformation();
                UpdateCameraBounds();
            }
        }

        /// <summary>
        /// Get or set the zoom level
        /// </summary>
        public static float Zoom
        {
            get
            {
                return _zoom;
            }
            set
            {
                _zoom = value;
                UpdateTransformation();
                UpdateCameraBounds();
            }
        }

        /// <summary>
        /// Get or set whether full screen is enabled or disabled
        /// </summary>
        public static bool IsFullScreen
        {
            get
            {
                return _graphics.IsFullScreen;
            }
            set
            {
                _graphics.IsFullScreen = value;
            }
        }

        /// <summary>
        /// Get or set whether the camera should be clamped to the world. If true, the 
        /// bounds of the camera will be forced to stay within the bounds of the world
        /// </summary>
        public static bool ClampCamera;

        /// <summary>
        /// Get or set the top-left position of the camera, in world coordinates.
        /// </summary>
        public static Point CameraLocation
        {
            get
            {
                return _cameraLocation;
            }
            set
            {
                _cameraLocation = value;

                if (ClampCamera)
                {
                    float zoomOffset = .5f / Zoom - .5f;
                    float offsetX = _cameraWidth * zoomOffset;
                    float offsetY = _cameraHeight * zoomOffset;

                    int x = (int)MathHelper.Clamp(_cameraLocation.X, offsetX, WorldSize.X - CameraWidth - offsetX);
                    int y = (int)MathHelper.Clamp(_cameraLocation.Y, offsetY, WorldSize.Y - CameraHeight - offsetY);


                    _cameraLocation = new Point(x, y);
                }

                _cameraLocationVector = new Vector2(_cameraLocation.X, _cameraLocation.Y);
                UpdateTransformation();
                UpdateCameraBounds();
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// The identity float function. This return the value given
        /// </summary>
        public static float Identity(float depth)
        {
            return depth;
        }

        /// <summary>
        /// Center the camera around the given point
        /// </summary>
        public static void CenterAt(Point center)
        {
            CameraLocation = new Point(center.X - (int)(CameraWidth / 2), center.Y - (int)(CameraHeight / 2));
        }

        /// <summary>
        /// Center the camera around the given area
        /// </summary>
        public static void CenterAt(Rectangle area)
        {
            CenterAt(area.Center);
        }

        /// <summary>
        /// Center the camera around the bounds of the given game object
        /// </summary>
        public static void CenterAt(GameObject gameObject)
        {
            CenterAt(gameObject.Bounds);
        }

        /// <summary>
        /// True if the given point is on the screen.
        /// </summary>
        public static bool Contains(Point point)
        {
            return _zoomedCameraBounds.Contains(point);
        }

        /// <summary>
        /// True if the given area is on the screen.
        /// </summary>
        public static bool Contains(Rectangle area)
        {
            return _zoomedCameraBounds.Intersects(area);
        }

        /// <summary>
        /// True if the bounds of the given game object is on the screen.
        /// </summary>
        public static bool Contains(GameObject gameObject)
        {
            return Contains(gameObject.Bounds);
        }

        /// <summary>
        /// Convert the given point from window coordinates to camera coordinates
        /// </summary>
        public static Point WindowToCamera(Point windowPoint)
        {
            float cameraScaleX = (float)_zoomedCameraBounds.Width / (float)_windowWidth;
            float cameraScaleY = (float)_zoomedCameraBounds.Height / (float)_windowHeight;

            int cameraX = (int)(windowPoint.X * cameraScaleX);
            int cameraY = (int)(windowPoint.Y * cameraScaleY);

            return new Point(cameraX, cameraY);
        }

        /// <summary>
        /// Convert the given point from camera coordinates to world coordinates
        /// </summary>
        public static Point CameraToWorld(Point cameraPoint)
        {
            return new Point(cameraPoint.X + _zoomedCameraBounds.X, cameraPoint.Y + _zoomedCameraBounds.Y);
        }

        /// <summary>
        /// Convert the given point from window coordinates to world coordinates
        /// </summary>
        public static Point WindowToWorld(Point windowPoint)
        {
            return CameraToWorld(WindowToCamera(windowPoint));
        }
        
        #region Draw

        public static void Draw(bool isGUI, Texture2D texture, Rectangle destinationRectangle, float drawDepth)
        {
            Draw(isGUI, texture, destinationRectangle, null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, drawDepth);
        }

        public static void Draw(bool isGUI, Texture2D texture, Rectangle destinationRectangle, Rectangle sourceRectangle, float drawDepth)
        {
            Draw(isGUI, texture, destinationRectangle, sourceRectangle, Color.White, 0f, Vector2.Zero, SpriteEffects.None, drawDepth);
        }

        public static void Draw(bool isGUI, Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float drawDepth)
        {
            if (isGUI)
            {
                destinationRectangle.Offset(_zoomedCameraBounds.Location);
            }
            else if (Contains(destinationRectangle))
            {
                _spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color, rotation, origin, effects, AdjustDrawDepth(drawDepth));
            }
        }

        public static void DrawString(bool isGUI, SpriteFont spriteFont, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float drawDepth)
        {
            if (isGUI)
            {
                position += _zoomedCameraLocationVector;
            }

            _spriteBatch.DrawString(spriteFont, text, position, color, rotation, origin, scale, effects, AdjustDrawDepth(drawDepth));
            
        }

        #endregion

        #endregion


        #region Internal Methods
        
        internal static void Initialize(GraphicsDeviceManager graphics)
        {
            _graphics = graphics;
            Zoom = 1.0f;
            WorldSize = new Point(graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
            _windowWidth = _cameraWidth = WorldSize.X;
            _windowHeight = _cameraHeight = WorldSize.Y;

            UpdateTransformation();
            UpdateCameraBounds();

            AdjustDrawDepth = Identity;
        }

        internal static void BeginSpriteBatch(SpriteBatch spriteBatch)
        {
            _spriteBatch = spriteBatch;
            _spriteBatch.Begin(SpriteSortMode.BackToFront, null, null, null, null, null, _cameraTransformation);
        }

        internal static void EndSpriteBatch()
        {
            _spriteBatch.End();
        }

        #endregion


        #region Private Methods

        private static void UpdateTransformation()
        {
            float cameraScaleX = (float)_windowWidth / (float) _cameraWidth;
            float cameraScaleY = (float)_windowHeight / (float) _cameraHeight;
            float zoomOffset = .5f - Zoom / 2;

            Matrix translateCamera = Matrix.CreateTranslation(-_cameraLocation.X, -_cameraLocation.Y, 0f);
            Matrix scaleCamera = Matrix.CreateScale(cameraScaleX, cameraScaleY, 1f);
            Matrix zoomCamera = Matrix.CreateScale(Zoom, Zoom, 1f);
            Matrix zoomOffsetCamera = Matrix.CreateTranslation(zoomOffset * _windowWidth, zoomOffset * _windowHeight, 0f);

            _cameraTransformation = translateCamera * scaleCamera * zoomCamera * zoomOffsetCamera;
        }

        private static void UpdateCameraBounds()
        {
            _zoomedCameraBounds = new Rectangle(_cameraLocation.X, _cameraLocation.Y, _cameraWidth, _cameraHeight);
            float zoomInflation = .5f / Zoom - .5f;

            _zoomedCameraBounds.Inflate((int)(zoomInflation * _cameraWidth), (int)(zoomInflation * _cameraHeight));
            _zoomedCameraLocationVector = new Vector2(_zoomedCameraBounds.X, _zoomedCameraBounds.Y);
        }

        #endregion
    }
}
